#!/usr/bin/perl -w

use strict;
use warnings;

use Gtk2 qw(-init);
use SDL::Mixer;

use constant APPNAME => 'Quizbiz';
use constant N_CONTESTANTS => 8;
use constant W_GAP  => 6; # padding around main window
use constant TH_GAP => 3; # between and around table widgets, horizontally
use constant TV_GAP => 1; # between and around table widgets, vertically
use constant DEBUG  => 0; # set true for commentary on stderr

warn "Starting up.\n"
    if DEBUG;
my $Mix = SDL::Mixer->new(-frequency => 44100, -channels => 2, -size => 1024)
    or die "Failed to create SDL mixer: $@";
&open_window;
warn "Exiting normally.\n"
    if DEBUG;
exit(0);


sub open_window {
    my $arb = Arbitrator->new(N_CONTESTANTS);
    my $window = Gtk2::Window->new;
    $window->set_title(APPNAME);
    $window->signal_connect(destroy => sub { Gtk2->main_quit });
    $window->set_border_width(W_GAP);
    $window->add($arb->widget);
    $window->add_accel_group($arb->accelgroup);
    $window->show_all;
    Gtk2->main;
}


# Convenience wrapper for Gtk2::Table::attach
sub Gtk2::Table::place {
    my ($table, $widget, $row, $col, $xpad, $ypad) = @_;
    $table->attach($widget, $col, $col + 1, $row, $row + 1, $xpad, $ypad, TH_GAP, TV_GAP);
}


# Arbitrator -- contestants, and buzzers, and controls, oh my!

package Arbitrator;

use constant W_GAP => 6; # gap between and around main widgets
use constant FALSE => '';
use constant TRUE => 1;
use constant CLEAR_KEY => 0xff1b; # key-code for Esc
use constant F1 => 0xffbe; # key-code for F1
use constant DEBUG => 0; # set true for commentary on stderr

sub new {
    my ($class, $n) = @_;
    my $self = bless({}, $class);
    $self->{status} = StatusDisplay->new;
    $self->{resetbutton} = Gtk2::Button->new('Clear');
    $self->{resetbutton}->signal_connect(clicked => sub { $self->clear });
    $self->{console} = Gtk2::HBox->new(FALSE, W_GAP);
    $self->{console}->pack_start($self->{status}->widget, TRUE, TRUE, 0);
    $self->{console}->pack_start($self->{resetbutton}, FALSE, TRUE, 0);
    $self->{contestants} = Contestants->new($n);
    $self->{widget} = Gtk2::VBox->new(FALSE, W_GAP);
    $self->{widget}->pack_start($self->{contestants}->widget, TRUE, TRUE, 0);
    $self->{widget}->pack_start($self->{console}, FALSE, FALSE, 0);
    $self->{accelgroup} = Gtk2::AccelGroup->new; # manages keystrokes
    $self->{accelgroup}->connect(CLEAR_KEY, [], [], sub { $self->clear });
    for my $i (0..$n-1) { $self->{accelgroup}->connect(F1 + $i, [], [], sub { $self->buzz($i) }) }
    return ($self);
}

sub widget { $_[0]->{widget} }

sub accelgroup { $_[0]->{accelgroup} }

sub clear {
    my ($self) = @_;
    $self->{contestants}->clear_all;
    $self->{status}->clear;
    delete $self->{winner};
}

sub buzz {
    my ($self, $n) = @_;
    warn __PACKAGE__, "->buzz($n)\n"
	if DEBUG;
    if (exists($self->{winner})) {
	$self->{contestants}->contestant($n)->lose
	    if $self->{winner} != $n;
    }
    else {
	$self->{winner} = $n;
	$self->{contestants}->contestant($n)->win;
	$self->{status}->display($self->{contestants}->contestant($n)->name || sprintf('F%d', $n+1));
    }
}


# StatusDisplay -- text area that tells you what's going on

package StatusDisplay;

sub new {
    my ($class) = @_;
    my $self = bless({}, $class);
    $self->{label} = Gtk2::Label->new;
    $self->{frame} = Gtk2::Frame->new;
    $self->{frame}->add($self->{label});
    $self->clear;
    return ($self);
}

sub widget { $_[0]->{frame} }

sub clear { $_[0]->{label}->set_markup('<i>Waiting for buzzers</i>') }

sub display {
    my ($self, $text) = @_;
    local $_ = $text;
    s/&/&amp;/g;
    s/</&lt;/g;
    s/>/&gt;/g;
    $self->{label}->set_markup("<b>$_</b>");
}


# Contestants -- all contestants, collectively, including the contestant table widget

package Contestants;

sub new {
    my ($class, $n) = @_;
    my $self = bless({}, $class);
    my @cols = qw(Light Name Sound Play);
    $self->{table} = Gtk2::Table->new($n + 1, scalar(@cols), 0);
    $self->{table}->set_border_width(0);
    for my $col (0..$#cols) {
	my $label = Gtk2::Label->new($cols[$col]);
	$self->{table}->place($label, 0, $col, 'fill', 'fill');
    }
    for (1..$n) {
	push @{$self->{contestants}}, Contestant->new->insert_in_table($self->{table}, $_);
    }
    return ($self);
}

sub widget { $_[0]->{table} }

sub contestant { $_[0]->{contestants}->[$_[1]] }

sub clear_all { for (@{$_[0]->{contestants}}) { $_->clear } }


# Contestant -- individual widgets, properties, and methods

package Contestant;

sub new {
    my ($class) = @_;
    my $self = bless({}, $class);
    $self->{light} = Indicator->new;
    $self->{name} = Gtk2::Entry->new;
    $self->{sound} = SoundWidget->new;
    return ($self);
}

sub insert_in_table {
    my ($self, $table, $row) = @_;
    $table->place($self->{light}->widget, $row, 0, 'fill', 'fill');
    $table->place($self->{name}, $row, 1, [qw(expand fill)], 'fill');
    $table->place($self->{sound}->load_button, $row, 2, [qw(expand fill)], 'fill');
    $table->place($self->{sound}->play_button, $row, 3, 'fill', 'fill');
    return ($self);
}

sub clear { $_[0]->{light}->clear }

sub win   { $_[0]->{sound}->play; $_[0]->{light}->win }

sub lose  { $_[0]->{light}->lose }

sub name  { $_[0]->{name}->get_text }


# Indicator -- composed widget: indicator light

package Indicator;

sub new {
    my ($class) = @_;
    my $self = bless({}, $class);
    $self->{colour} = Gtk2::DrawingArea->new;
    $self->{frame} = Gtk2::Frame->new;
    $self->{frame}->set_shadow_type('out');
    $self->{frame}->set_border_width(5);
    $self->{frame}->add($self->{colour});
    $self->clear;
    return ($self);
}

sub widget { $_[0]->{frame} }

sub set_colour {
    my ($self, $colour) = @_;
    $self->{colour}->modify_bg('normal', Gtk2::Gdk::Color->parse($colour));
}

sub clear { set_colour($_[0], '#808080') }

sub win   { set_colour($_[0], '#6f6') }

sub lose  { set_colour($_[0], '#f66') }


# SoundWidget -- two-part compound widget: load and play sounds, with preview

package SoundWidget;

sub new {
    my ($class) = @_;
    my $self = bless({}, $class);
    $self->{loadbutton} = Gtk2::FileChooserButton->new('Select a sound file', 'open');
    my $preview_button = Gtk2::ToolButton->new_from_stock('gtk-media-play');
    $self->{loadbutton}->set_preview_widget($preview_button);
    $preview_button->signal_connect(clicked => sub { $self->play_preview });
    $self->{loadbutton}->signal_connect('update-preview' => sub { $self->load_preview });
    $self->{loadbutton}->signal_connect('file-set' => sub { $self->unload_preview; $self->load });
    $self->{playbutton} = Gtk2::ToolButton->new_from_stock('gtk-media-play');
    $self->{playbutton}->signal_connect(clicked => sub { $self->play });
    $self->{playbutton}->set_sensitive(0);
    $self->{preview} = SoundSlot->new;
    $self->{sound} = SoundSlot->new;
    return ($self);
}

sub load_button { $_[0]->{loadbutton} }

sub play_button { $_[0]->{playbutton} }

sub load_preview {
    my ($self) = @_;
    $self->{preview}->load($self->{loadbutton}->get_preview_filename);
    $self->{loadbutton}->set_preview_widget_active($self->{preview}->is_loaded);
}

sub unload_preview { $_[0]->{preview}->unload }

sub play_preview { $_[0]->{preview}->play }

sub load {
    my ($self) = @_;
    $self->{sound}->load($self->{loadbutton}->get_filename);
    $self->{playbutton}->set_sensitive($self->{sound}->is_loaded);
}

sub unload { $_[0]->{sound}->unload }

sub play { $_[0]->{sound}->play }


# SoundSlot -- may or may not be loaded with a sound

package SoundSlot;

use constant DEBUG => 0; # set true for commentary on stderr

sub new {
    my ($class) = @_;
    my $self;
    return (bless(\$self, $class));
}

sub load {
    my ($self, $file) = @_;
    $$self = eval {
	die "filename is undef\n" if not defined($file);
	die "filename is empty\n" if $file eq '';
	die "'$file' is not a file\n" if not -f $file;
	SDL::Sound->new($file);
    };
    warn __PACKAGE__, "->load: ", $$self ? "'$file' loaded\n" : $@
	if DEBUG;
}

sub unload { undef ${$_[0]} }

sub is_loaded { defined(${$_[0]}) }

sub play {
    my ($self) = @_;
    if ($$self) { $Mix->play_channel(0, $$self, 0) }
    elsif (DEBUG) { warn __PACKAGE__, "->play: no sample loaded\n" }
}
